﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Opennav.MapGenerator.OSM;
using Opennav.MapGenerator.Geometry;

namespace Opennav.MapGenerator.OSM
{
    public class BinNodesCache : IEnumerable<OSMNode>
    {
        private FileStream _nodeStream;
        private FileStream _idxStream;
        private int idxElementSize = sizeof(uint) + 2 * sizeof(int) + sizeof(long);
        private uint lastId = 0;

        private string nodeFile, indexFile;

        public BinNodesCache(string tempFolder, bool reopen)
        {
            FileMode mode = FileMode.Create;
            if (reopen)
                mode = FileMode.Open;
            nodeFile = Path.Combine(tempFolder, "nodes.tmp");
            indexFile = Path.Combine(tempFolder, "nodes.x");
            _nodeStream = new FileStream(nodeFile, mode, FileAccess.ReadWrite);
            _idxStream = new FileStream(indexFile, mode, FileAccess.ReadWrite);
            if (Count > 0)
            {
                _idxStream.Seek((Count - 1) * idxElementSize, SeekOrigin.Begin);
                lastId = (new BinaryReader(_idxStream)).ReadUInt32();
            }
        }

        public void Close()
        {
            _nodeStream.Flush();
            _nodeStream.Close();
            _idxStream.Flush();
            _idxStream.Close();
            File.Delete(nodeFile);
            File.Delete(indexFile);
        }

        public void Add(OSMNode node)
        {
            if (lastId >= node.Id)
                throw new InvalidOperationException("nodes out of order");
            lastId = node.Id;
            BinaryWriter idxWriter = new BinaryWriter(_idxStream);
            idxWriter.Seek(0, SeekOrigin.End);
            BinaryWriter nodeWriter = new BinaryWriter(_nodeStream);
            nodeWriter.Seek(0, SeekOrigin.End);
            long offset = nodeWriter.BaseStream.Position;
            nodeWriter.Write((ushort)node.Tags.Count);
            foreach (OSMTag tag in node.Tags)
            {
                nodeWriter.Write(tag.Key);
                nodeWriter.Write(tag.Value);
            }
            idxWriter.Write(node.Id);
            idxWriter.Write(node.Coordinate.Longitude);
            idxWriter.Write(node.Coordinate.Latitude);
            idxWriter.Write(offset);
        }

        public Coordinate GetCoordinate(uint id)
        {
            if (SeekIdx(id))
                return ReadCoordinate(id);
            else
                return null;
        }

        public OSMNode GetNode(uint id)
        {
            if (SeekIdx(id))
                return ReadNode(id);
            else
                return null;
        }

        public int Count
        {
            get
            {
                return (int)(_idxStream.Length / idxElementSize);
            }
        }

        public IList<Coordinate> ResolveCoordinates(OSMWay way)
        {
            if (way == null) return null;
            IList<Coordinate> coordinates = new List<Coordinate>();
            foreach (uint nodeId in way.Nodes)
            {
                Coordinate coordinate = GetCoordinate(nodeId);
                if (coordinate == null)
                    return null;
                coordinates.Add(coordinate);
            }
            return coordinates;
        }

        public IList<OSMNode> ResolveNodes(OSMWay way)
        {
            if (way == null) return null;
            IList<OSMNode> coordinates = new List<OSMNode>();
            foreach (uint nodeId in way.Nodes)
            {
                OSMNode node = GetNode(nodeId);
                if (node == null)
                    return null;
                coordinates.Add(node);
            }
            return coordinates;
        }

        private bool SeekIdx(uint id)
        {
            BinaryReader reader = new BinaryReader(_idxStream);
            int middle;
            int left = 0;
            int elements = Count;
            int right = elements - 1;
            while (true)
            {
                middle = left + ((right - left) / 2);
                if (right < left)
                {
                    return false;
                }
                reader.BaseStream.Seek(middle * idxElementSize, SeekOrigin.Begin);
                uint nodeId = reader.ReadUInt32();
                if (nodeId == id)
                {
                    return true;
                }
                if (nodeId > id)
                    right = middle - 1;
                else
                    left = middle + 1;
            }
        }

        private Coordinate ReadCoordinate(uint id)
        {
            BinaryReader idxReader = new BinaryReader(_idxStream);
            int longitude = idxReader.ReadInt32();
            int latitude = idxReader.ReadInt32();
            return new Coordinate(longitude, latitude);
        }

        private OSMNode ReadNode(uint id)
        {
            BinaryReader idxReader = new BinaryReader(_idxStream);
            int longitude = idxReader.ReadInt32();
            int latitude = idxReader.ReadInt32();
            long offset = idxReader.ReadInt64();
            BinaryReader nodeReader = new BinaryReader(_nodeStream);
            nodeReader.BaseStream.Seek(offset, SeekOrigin.Begin);
            ushort ntags = nodeReader.ReadUInt16();
            IList<OSMTag> tags = new List<OSMTag>();
            for (int ntag = 0; ntag < ntags; ntag++)
            {
                string key = nodeReader.ReadString();
                string value = nodeReader.ReadString();
                tags.Add(new OSMTag() { Key = key, Value = value });
            }
            return new OSMNode() { Tags = tags, Coordinate = new Coordinate(longitude, latitude), Id = id };
        }

        public IEnumerator<OSMNode> GetEnumerator()
        {
            for (int i = 0; i < Count; i++)
            {
                _idxStream.Seek(i * idxElementSize, SeekOrigin.Begin);
                uint nodeId = (new BinaryReader(_idxStream)).ReadUInt32();
                yield return ReadNode(nodeId);
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}
